-
Notifications
You must be signed in to change notification settings - Fork 15.3k
[mlir] Add Normalize pass #162266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[mlir] Add Normalize pass #162266
Conversation
|
✅ With the latest revision this PR passed the C/C++ code formatter. |
d14a41f to
0836dad
Compare
jpienaar
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you expand the PR desciption as well as pass and function comments to capture the goals, what's working etc?
|
|
||
| uint64_t Hash = MagicHashConstant; | ||
|
|
||
| uint64_t opcodeHash = strHash(op->getName().getStringRef().str()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StringRef has hash_value , could that be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. iirc, set_fixed_execution_hash_seed has been removed in the latest tree which means llvm::hash_value currently gives different hash in subsequent runs (unless you rebuild llvm with certain flags). I believe the user would want the SSA names to be consistent throughout runs.
| if (op.hasTrait<OpTrait::IsTerminator>()) | ||
| return true; | ||
|
|
||
| if (auto memOp = dyn_cast<MemoryEffectOpInterface>(&op)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you comment as to why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check the updated description once and let me know if its still not clear ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment should be in the code too, not just description. But no it doesn't really explain why. It just says it is done. IRNormalizer.cpp makes it clearer as not to reorder around side-effecting operations.
| Operands.push_back({Stream.str(), operand}); | ||
| } | ||
|
|
||
| if (op->hasTrait<OpTrait::IsCommutative>()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commutative operations are naturally reordered while considering constants, would this be considered here too giving naming conventions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the constants would have been hashed during the nameAsInitialOperation call, so if the operation is commutative, the constant operands would get reordered accordingly.
|
|
||
| // CHECK-LABEL: module { | ||
| // CHECK: func.func @infinte_loop(%[[ARG0:.*]]: memref<?xi32>, %[[ARG1:.*]]: i32) { | ||
| // CHECK: %vl15969$e5677$ = arith.constant 1 : i32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's up with these cryptic SSA variable names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the point of llvm-canon, it gives everything predictable "canonical" names so when you compare two files textually, they don't differ because of SSA value numbers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can refer to this talk to further understand the motivation behind llvm-canon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, its useful for that but indeed terrible to read :) (I almost wonder if the diff tool post would be smart enough to reduce it to something smaller ... or perhaps give both files in to diffing so that names could be chosen with more care - e.g., take all names across both, rename across both s/%vl15390$funcArg1-vl15969$/%0/ etc)
| private: | ||
| const uint64_t MagicHashConstant = 0x6acaa36bef8325c5ULL; | ||
| void | ||
| collectOutputOperations(Block &block, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of the functions here should have documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you may have forgotten to upload the change.
I also like how LLVM's pass grouped these (llvm/lib/Transforms/Utils/IRNormalizer.cpp)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added the documentation above the definition of each function. Though I left 2-3 functions whose definitions were short or their functionality was intuitive from the name.
ftynse
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your patch! Please provide a clear commit description, explaining why you make changes, not only what they are: https://mlir.llvm.org/getting_started/Contributing/#commit-messages.
Two large items that will need addressing is (1) this does not belong to lib/Conversion because it is not a conversion and it will have to operate generally on all MLIR operations regardless of dialects and (2) we need to find a way of testing the desired goal of the tool which is minizing textual difference between files to "semantically meaningful" parts.
Should we make a separate tool out of it or place it in some other directory like rewrite..?
We can try making 2 forms of the same initial code by applying 1-2 different optimizations and expect small diffs because of them being semantically similar as mentioned here for mir canon |
jpienaar
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any test with side-effecting ops
| //===----------------------------------------------------------------------===// | ||
| // Normalize | ||
| //===----------------------------------------------------------------------===// | ||
| def Normalize : Pass<"normalize", "ModuleOp"> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it required to be a ModuleOp pass? Does it need to be at symbol table scope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well we do need the top-level module operation to collect all the output operations and thus reorder/rename regular/initial operations if they have not been visited earlier by another output operation using them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow this. I may be missing what you are saying as I think this is true and could work even for functions/doesn't even need to be on top-level ops. But it does need to (correct me if I'm wrong) run sequentially due to rng update ordering.
| visited.insert(op); | ||
|
|
||
| if (isOutput(*op)) { | ||
| func::FuncOp func = op->getParentOfType<func::FuncOp>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can avoid making this on FuncOp and just query the Block in which this operation is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But we need the enclosing function to get different footprint for output operations at different depth. If we measure the footprint from block, then two output operations at same depth from their enclosing block would have same footprint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true for a FuncOp too, but what is indeed different is that FuncOp is IsolatedFromAbove. Would that be sufficient?
My goal here is to not hardcode an assumption of 1 top level module with N functions underneath. But enable it to run even where there is another top level op and other region ops.
| /// Helper method returning indices (distance from the beginning of the basic | ||
| /// block) of output operations using the given operation. Walks down the | ||
| /// def-use tree recursively | ||
| llvm::SetVector<int> NormalizePass::getOutputFootprint( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this used below? Seems to be for hashing, but not sure I followed the logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the outputfootprint is used to differentiate between initial operations that have the same def but used by different output operations.
|
|
||
| // CHECK-LABEL: module { | ||
| // CHECK: func.func @infinte_loop(%[[ARG0:.*]]: memref<?xi32>, %[[ARG1:.*]]: i32) { | ||
| // CHECK: %vl15969$e5677$ = arith.constant 1 : i32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, its useful for that but indeed terrible to read :) (I almost wonder if the diff tool post would be smart enough to reduce it to something smaller ... or perhaps give both files in to diffing so that names could be chosen with more care - e.g., take all names across both, rename across both s/%vl15390$funcArg1-vl15969$/%0/ etc)
| %tmp41 = arith.xori %tmp40, %cneg1 : i32 | ||
| %tmp42 = arith.addi %tmp39, %tmp41 : i32 | ||
| %tmp43 = arith.addi %tmp42, %c0 : i32 | ||
| %tmp44 = arith.muli %tmp43, %tmp40 : i32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of these seem like they can't be reordered due to creating a rather linear sequence here. How about doing a N node tree-link input structure here linearized, that way you could have both short and long term reorderings? (of course many other generated could work, goal is just to know there are many variants possible and given two variants see same output).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well this test was largely inspired by a similar test in llvm-canon pr : https://github.com/llvm/llvm-project/pull/113780/files#diff-25aa6a3cb4c2af6c07edea9fa7b12a1afefa1337b2bb1802cb4658a3a2fb6353 .
But I have modified our infinite-loop test into 2 variants with tree-like dependencies that will produce the same canonical form after the normalize pass reorders them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not seeing it, I see
%tmp23 = arith.muli %tmp22, %tmp19 : i32
%tmp24 = arith.xori %tmp23, %cneg1 : i32
%tmp25 = arith.addi %tmp22, %tmp24 : i32
%tmp26 = arith.addi %tmp25, %c0 : i32
%tmp27 = arith.muli %tmp26, %tmp23 : i32
%tmp28 = arith.xori %tmp27, %cneg1 : i32
%tmp29 = arith.addi %tmp26, %tmp28 : i32
%tmp30 = arith.addi %tmp29, %c0 : i32
...and you have 24 depend on 23, 25 on 24. 26 on 25, 27 on 26, 28 on 27, 29 on 27, 30 on 29 - so here there is no reordering possible in this snippet.
… that will produce the same canonical form after the normalize pass reorders them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just +1 to Alex's point that this isn't a Conversion in MLIR terminology (https://mlir.llvm.org/getting_started/Glossary/#conversion). Transforms could be better spot, although I was indeed also wondering if in Tools directory wouldn't be better as it is rather specific when one runs this.
| %tmp41 = arith.xori %tmp40, %cneg1 : i32 | ||
| %tmp42 = arith.addi %tmp39, %tmp41 : i32 | ||
| %tmp43 = arith.addi %tmp42, %c0 : i32 | ||
| %tmp44 = arith.muli %tmp43, %tmp40 : i32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not seeing it, I see
%tmp23 = arith.muli %tmp22, %tmp19 : i32
%tmp24 = arith.xori %tmp23, %cneg1 : i32
%tmp25 = arith.addi %tmp22, %tmp24 : i32
%tmp26 = arith.addi %tmp25, %c0 : i32
%tmp27 = arith.muli %tmp26, %tmp23 : i32
%tmp28 = arith.xori %tmp27, %cneg1 : i32
%tmp29 = arith.addi %tmp26, %tmp28 : i32
%tmp30 = arith.addi %tmp29, %c0 : i32
...and you have 24 depend on 23, 25 on 24. 26 on 25, 27 on 26, 28 on 27, 29 on 27, 30 on 29 - so here there is no reordering possible in this snippet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you rerun the input through mlir-opt? (the space just feels off here).
| return true; | ||
| } | ||
|
|
||
| if (auto call = dyn_cast<func::CallOp>(op)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've resolved the comment, but I still see this on func::CallOp
This PR aims to add a pass that reduces the diff noise between two roughly semantically similar MLIR modules. We do this in a 2 step process. The first is instruction reordering. Now two semanitcally similar modules should have roughly the same type of side-effects and the same structure of control flow graph. So we collect operations (termed as output, which include operations with an
IsTerminatortrait, operations with aMemoryEffects::Writeside effect, and call operation) and walk them top-down recursively so that for each operand, we bring their definition as close as possible to the using operation. The second is deterministic SSA naming. Simple linear naming won't work because the addition of any new operation in two roughly semantically similar modules would change the diff a lot. The naming scheme we have chosen derives from the one introduced in llvm-canon (refer to this talk for further details). There is one aspect of it that might need some discussion on. While naming initial operations, we collect outputFootprint as the distance of output operations from the beginning of the function. But this means that the addition of any redundant operation will change the name of all the inital operations which would pollute the diff a lot. Instead, perhaps we can simply add the number of output operations using that initial operation in the hash ?Patch made in collaboration with @anant37289